
package org.apache.solr.servlet;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MultiMapSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.util.ContentStreamBase;
import org.apache.solr.core.Config;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.ServletSolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrQueryRequestBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SolrRequestParsers {

    final Logger log = LoggerFactory.getLogger(SolrRequestParsers.class);
    // Should these constants be in a more public place?
    public static final String MULTIPART = "multipart";
    public static final String RAW = "raw";
    public static final String SIMPLE = "simple";
    public static final String STANDARD = "standard";
    private HashMap<String, SolrRequestParser> parsers;
    private boolean enableRemoteStreams = false;
    private boolean handleSelect = true;
    private StandardRequestParser standard;

    // Pass in an xml configuration. A null configuration will enable everythign with maximum values.
    public SolrRequestParsers(Config globalConfig) {

        long uploadLimitKB = 1048;  // 2MB default
        if (globalConfig == null) {
            uploadLimitKB = Long.MAX_VALUE;
            enableRemoteStreams = true;
            handleSelect = true;
        }
        else {
            uploadLimitKB = globalConfig.getInt("requestDispatcher/requestParsers/@multipartUploadLimitInKB", (int) uploadLimitKB);

            enableRemoteStreams = globalConfig.getBool("requestDispatcher/requestParsers/@enableRemoteStreaming", false);

            // Let this filter take care of /select?xxx format
            handleSelect = globalConfig.getBool("requestDispatcher/@handleSelect", handleSelect);
        }

        MultipartRequestParser multi = new MultipartRequestParser(uploadLimitKB);
        RawRequestParser raw = new RawRequestParser();
        standard = new StandardRequestParser(multi, raw);

        // I don't see a need to have this publicly configured just yet
        // adding it is trivial
        parsers = new HashMap<>();
        parsers.put(MULTIPART, multi);
        parsers.put(RAW, raw);
        parsers.put(SIMPLE, new SimpleRequestParser());
        parsers.put(STANDARD, standard);
        parsers.put("", standard);
    }

    public SolrQueryRequest parse(SolrCore core, String path, HttpServletRequest req) throws Exception {

        SolrRequestParser parser = standard;

        // TODO -- in the future, we could pick a different parser based on the request

        // Pick the parser from the request...
        ArrayList<ContentStream> streams = new ArrayList<>(1);
        SolrParams params = parser.parseParamsAndFillStreams(req, streams);
        SolrQueryRequest sreq = buildRequestFrom(core, params, streams);

        // Handlers and login will want to know the path. If it contains a ':'
        // the handler could use it for RESTful URLs
        sreq.getContext().put("path", path);
        return sreq;
    }

    public SolrQueryRequest buildRequestFrom(SolrCore core, SolrParams params, Collection<ContentStream> streams) throws Exception {

        // The content type will be applied to all streaming content
        String contentType = params.get(CommonParams.STREAM_CONTENTTYPE);

        // Handle anything with a remoteURL
        String[] strs = params.getParams(CommonParams.STREAM_URL);
        if (strs != null) {
            if (!enableRemoteStreams) {
                throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Remote Streaming is disabled.");
            }
            for (final String url : strs) {
                ContentStreamBase stream = new ContentStreamBase.URLStream(new URL(url));
                if (contentType != null) {
                    stream.setContentType(contentType);
                }
                streams.add(stream);
            }
        }

        // Handle streaming files
        strs = params.getParams(CommonParams.STREAM_FILE);
        if (strs != null) {
            if (!enableRemoteStreams) {
                throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Remote Streaming is disabled.");
            }
            for (final String file : strs) {
                ContentStreamBase stream = new ContentStreamBase.FileStream(new File(file));
                if (contentType != null) {
                    stream.setContentType(contentType);
                }
                streams.add(stream);
            }
        }

        // Check for streams in the request parameters
        strs = params.getParams(CommonParams.STREAM_BODY);
        if (strs != null) {
            for (final String body : strs) {
                ContentStreamBase stream = new ContentStreamBase.StringStream(body);
                if (contentType != null) {
                    stream.setContentType(contentType);
                }
                streams.add(stream);
            }
        }

        SolrQueryRequestBase q = new SolrQueryRequestBase(core, params) { };
        if (streams != null && streams.size() > 0) {
            q.setContentStreams(streams);
        }
        return q;
    }

    // Given a standard query string map it into solr params
    public static MultiMapSolrParams parseQueryString(String queryString) {

        Map<String, String[]> map = new HashMap<>();
        if (queryString != null && queryString.length() > 0) {
            try {
                for (String kv : queryString.split("&")) {
                    int idx = kv.indexOf('=');
                    if (idx > 0) {
                        String name = URLDecoder.decode(kv.substring(0, idx), "UTF-8");
                        String value = URLDecoder.decode(kv.substring(idx + 1), "UTF-8");
                        MultiMapSolrParams.addParam(name, value, map);
                    }
                    else {
                        String name = URLDecoder.decode(kv, "UTF-8");
                        MultiMapSolrParams.addParam(name, "", map);
                    }
                }
            }
            catch (UnsupportedEncodingException uex) {
                throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, uex);
            }
        }
        return new MultiMapSolrParams(map);
    }

    public boolean isHandleSelect() {
        return handleSelect;
    }

    public void setHandleSelect(boolean handleSelect) {
        this.handleSelect = handleSelect;
    }
}

// I guess we don't really even need the interface, but i'll keep it here just for kicks
interface SolrRequestParser {

    public SolrParams parseParamsAndFillStreams(
            final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception;
}

/**
 * The simple parser just uses the params directly
 */
class SimpleRequestParser implements SolrRequestParser {

    @Override
    public SolrParams parseParamsAndFillStreams(
            final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception {
        return new ServletSolrParams(req);
    }
}


// Wrap an HttpServletRequest as a ContentStream
class HttpRequestContentStream extends ContentStreamBase {

    private final HttpServletRequest req;

    public HttpRequestContentStream(HttpServletRequest req) {
        this.req = req;

        contentType = req.getContentType();
        // name = ???
        // sourceInfo = ???

        String v = req.getHeader("Content-Length");
        if (v != null) {
            size = Long.valueOf(v);
        }
    }

    @Override
    public InputStream getStream() throws IOException {
        return req.getInputStream();
    }
}

/**
 * Wrap a FileItem as a ContentStream
 */
class FileItemContentStream extends ContentStreamBase {

    private final FileItem item;

    public FileItemContentStream(FileItem f) {
        item = f;
        contentType = item.getContentType();
        name = item.getName();
        sourceInfo = item.getFieldName();
        size = item.getSize();
    }

    @Override
    public InputStream getStream() throws IOException {
        return item.getInputStream();
    }
}


 // The raw parser just uses the params directly
class RawRequestParser implements SolrRequestParser {

    @Override
    public SolrParams parseParamsAndFillStreams(final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception {
        // The javadocs for HttpServletRequest are clear that req.getReader() should take
        // care of any character encoding issues.  BUT, there are problems while running on
        // some servlet containers: including Tomcat 5 and resin.
        //
        // Rather than return req.getReader(), this uses the default ContentStreamBase method
        // that checks for charset definitions in the ContentType.

        streams.add(new HttpRequestContentStream(req));

        return SolrRequestParsers.parseQueryString(req.getQueryString());
    }
}


// Extract Multipart streams
class MultipartRequestParser implements SolrRequestParser {

    private long uploadLimitKB;

    public MultipartRequestParser(long limit) {
        uploadLimitKB = limit;
    }

    @Override
    public SolrParams parseParamsAndFillStreams(final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception {

        if (!ServletFileUpload.isMultipartContent(req)) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Not multipart content! " + req.getContentType());
        }

        MultiMapSolrParams params = SolrRequestParsers.parseQueryString(req.getQueryString());

        // Create a factory for disk-based file items
        DiskFileItemFactory factory = new DiskFileItemFactory();

        // Set factory constraints
        // TODO - configure factory.setSizeThreshold(yourMaxMemorySize);
        // TODO - configure factory.setRepository(yourTempDirectory);

        // Create a new file upload handler
        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setSizeMax(uploadLimitKB * 1024);

        // Parse the request
        List items = upload.parseRequest(req);
        Iterator iter = items.iterator();
        while (iter.hasNext()) {
            FileItem item = (FileItem) iter.next();

            // If its a form field, put it in our parameter map
            if (item.isFormField()) {
                MultiMapSolrParams.addParam(item.getFieldName(), item.getString(), params.getMap());
            } // Add the stream
            else {
                streams.add(new FileItemContentStream(item));
            }
        }
        return params;
    }
}


// The default Logic
class StandardRequestParser implements SolrRequestParser {

    MultipartRequestParser multipart;
    RawRequestParser raw;

    StandardRequestParser(MultipartRequestParser multi, RawRequestParser raw) {
        this.multipart = multi;
        this.raw = raw;
    }

    @Override
    public SolrParams parseParamsAndFillStreams(final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception {

        String method = req.getMethod().toUpperCase(Locale.ROOT);
        if ("GET".equals(method) || "HEAD".equals(method)) {
            return new ServletSolrParams(req);
        }
        if ("POST".equals(method)) {
            String contentType = req.getContentType();
            if (contentType != null) {
                int idx = contentType.indexOf(';');
                if (idx > 0) { // remove the charset definition "; charset=utf-8"
                    contentType = contentType.substring(0, idx);
                }
                if ("application/x-www-form-urlencoded".equals(contentType.toLowerCase(Locale.ROOT))) {
                    return new ServletSolrParams(req); // just get the params from parameterMap
                }
                if (ServletFileUpload.isMultipartContent(req)) {
                    return multipart.parseParamsAndFillStreams(req, streams);
                }
            }
            return raw.parseParamsAndFillStreams(req, streams);
        }
        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unsupported method: " + method);
    }
}
